package joc.internal.transformer;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javassist.CtBehavior;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMember;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.AccessFlag;
import joc.JOCEnabled;
import joc.customize.Contract;
import joc.customize.ObjectAwareContract;
import joc.customize.ParameterAwareContract;
import joc.customize.StateRetainingContract;
import joc.internal.EvaluationResult;
import joc.internal.Evaluator;
import joc.internal.ObjectConverter;
import joc.internal.ThreadAwareConditionCache;
import joc.internal.compiler.ArrayExp;
import joc.internal.compiler.BooleanExp;
import joc.internal.compiler.CastExp;
import joc.internal.compiler.ConstructorExp;
import joc.internal.compiler.IfExp;
import joc.internal.compiler.NestedExp;
import joc.internal.compiler.StandaloneExp;
import joc.internal.compiler.StaticCallExp;
import joc.internal.compiler.TryExp;
import joc.internal.compiler.ValueExp;
import joc.internal.util.AnnotationInstantiator;

import org.apache.log4j.Logger;

public abstract class ClassTransformer {
	private Logger logger = Logger.getLogger(getClass());
	protected Transformer transformer;

	public ClassTransformer(Transformer transformer) {
		this.transformer = transformer;
	}

	public abstract void transform(CtClass clazz) throws Exception;

	public List<Method> getContractMethodsWithAnnotation(Class<? extends Contract<?>> contractClass,
			Class<?>... annotationClasses) {
		List<Method> methods = new ArrayList<Method>();
		for (Method contractMethod : contractClass.getMethods()) {
			for (Class<?> annotationClass : annotationClasses) {
				@SuppressWarnings("unchecked")
				Class<? extends Annotation> annotationClassCasted = (Class<? extends Annotation>) annotationClass;
				Annotation methodAnnotation = contractMethod.getAnnotation(annotationClassCasted);
				if (methodAnnotation != null) {
					methods.add(contractMethod);
				}
			}
		}
		return methods;
	}

	private NestedExp getAnnotationValueExp(Annotation annotation, String name) throws Exception {
		getLogger().info("invoking method " + name + " on annotation " + annotation.getClass().getName());
		return new ValueExp(annotation.getClass().getMethod(name).invoke(annotation), annotation.getClass()
				.getMethod(name).getReturnType());
	}

	public StandaloneExp getContractCallString(CtBehavior behavior, Method contractMethod,
			Class<? extends Contract<?>> contractClass, Class<? extends Annotation> annotationClass,
			Annotation annotation, boolean evaluate, NestedExp... moreArgs) throws Exception {
		return getContractCallString(false, behavior, contractMethod, contractClass, annotationClass, annotation,
				evaluate, moreArgs);
	}

	public StandaloneExp getContractCallString(CtField affectedField, Method contractMethod,
			Class<? extends Contract<?>> contractClass, Class<? extends Annotation> annotationClass,
			Annotation annotation, boolean evaluate, NestedExp... moreArgs) throws Exception {
		return getContractCallString(false, affectedField, null, contractMethod, contractClass, annotationClass,
				annotation, evaluate, moreArgs);
	}

	public StandaloneExp getContractCallString(boolean initialize, CtBehavior behavior, Method contractMethod,
			Class<? extends Contract<?>> contractClass, Class<? extends Annotation> annotationClass,
			Annotation annotation, boolean evaluate, NestedExp... moreArgs) throws Exception {
		return getContractCallString(initialize, behavior, behavior, contractMethod, contractClass, annotationClass,
				annotation, evaluate, moreArgs);
	}

	private StandaloneExp getContractCallString(boolean initialize, CtMember affectedMember, CtBehavior behavior,
			Method contractMethod, Class<? extends Contract<?>> contractClass,
			Class<? extends Annotation> annotationClass, Annotation annotation, boolean evaluate, NestedExp... moreArgs)
			throws Exception {
		Map<String, NestedExp> annotationValues = new HashMap<String, NestedExp>();
		getLogger().info(
				"creating callString for method " + contractMethod.getName() + " and annotation "
						+ annotationClass.getName());
		for (Method annotationMethod : annotationClass.getDeclaredMethods()) {
			annotationValues.put(annotationMethod.getName(),
					getAnnotationValueExp(annotation, annotationMethod.getName()));
		}
		List<String> sortedAnnotationValueKeys = new ArrayList<String>(annotationValues.keySet());
		Collections.sort(sortedAnnotationValueKeys);
		List<NestedExp> sortedAnnotationValues = new ArrayList<NestedExp>();
		for (String key : sortedAnnotationValueKeys) {
			sortedAnnotationValues.add(new StaticCallExp(ObjectConverter.toObject, annotationValues.get(key)));
		}

		ValueExp cacheIdentifier = new ValueExp(contractMethod.getName());
		ValueExp contractIdentifier = new ValueExp(contractClass);
		IfExp initCondition = initializeCache(contractIdentifier, cacheIdentifier, new ValueExp(""));
		IfExp activeCondition = new IfExp(new StaticCallExp(ThreadAwareConditionCache.isConditionActive,
				contractIdentifier, cacheIdentifier));
		activeCondition.addIfBody(new StaticCallExp(ThreadAwareConditionCache.deactivateCondition, contractIdentifier,
				cacheIdentifier).toStandalone());

		NestedExp contractInst = new ConstructorExp(contractClass);
		if (StateRetainingContract.class.isAssignableFrom(contractClass)) {
			String contractIdentifierString = behavior.getLongName() + "@" + annotationClass.getName();
			if (initialize) {
				activeCondition.addIfBody(new StaticCallExp(ThreadAwareConditionCache.saveCondition, NestedExp.THIS,
						new ValueExp(contractIdentifierString), contractInst).toStandalone());
			}
			contractInst = new CastExp(contractClass, new StaticCallExp(ThreadAwareConditionCache.getCondition,
					NestedExp.THIS, new ValueExp(contractIdentifierString)));
		}
		NestedExp annotationInstance;
		if (sortedAnnotationValues.isEmpty()) {
			annotationInstance = new StaticCallExp(AnnotationInstantiator.getAnnotationArrayAccessible, new ValueExp(
					annotationClass));
		} else {
			annotationInstance = new StaticCallExp(AnnotationInstantiator.getAnnotationArrayAccessible, new ValueExp(
					annotationClass), new ArrayExp(Object.class, sortedAnnotationValues));
		}
		List<NestedExp> callParams = new ArrayList<NestedExp>();
		callParams.add(new CastExp(annotationClass, annotationInstance));
		callParams.addAll(Arrays.asList(moreArgs));
		if (ObjectAwareContract.class.isAssignableFrom(contractClass)) {
			callParams.add(NestedExp.THIS);
		}
		if (ParameterAwareContract.class.isAssignableFrom(contractClass)) {
			for (int i = 1; i <= behavior.getParameterTypes().length; i++) {
				callParams.add(NestedExp.arg(i));
			}
		}
		NestedExp contractCall = contractInst.appendCall(contractMethod.getName(), callParams);
		StandaloneExp activationCall = new StaticCallExp(ThreadAwareConditionCache.activateCondition,
				contractIdentifier, cacheIdentifier).toStandalone();
		if (evaluate) {
			NestedExp evaluationResult = new ConstructorExp(EvaluationResult.class, new ValueExp(
					getRecipientString(affectedMember)), contractCall);
			StandaloneExp evaluationCall = new StaticCallExp(Evaluator.evaluateContract, evaluationResult)
					.toStandalone();
			TryExp activationFallback = new TryExp(evaluationCall);
			activationFallback.addFinally(activationCall);
			activeCondition.addIfBody(activationFallback);
		} else {
			activeCondition.addIfBody(contractCall.toStandalone());
			activeCondition.addIfBody(activationCall);
		}

		StandaloneExp exp = initCondition.append(activeCondition);

		getLogger().info("callString: " + exp.getCode());
		return exp;
	}

	public boolean hasInheritedAnnotation(CtMethod method, Class<? extends Annotation> annotationClass)
			throws NotFoundException {
		for (CtBehavior consultedBehavior : getConsultedBehaviors(method)) {
			if (consultedBehavior.hasAnnotation(annotationClass)) {
				getLogger().debug(method.getLongName() + " is pure from " + consultedBehavior.getLongName());
				return true;
			}
		}
		return false;
	}

	protected NestedExp handlePrimitiveExp(CtClass clazz, NestedExp exp) {
		if (clazz.isPrimitive()) {
			return new StaticCallExp(ObjectConverter.toPrimitive, exp);
		}
		return new StaticCallExp(ObjectConverter.toObject, exp);
	}

	protected NestedExp handlePrimitiveExp(Class<?> clazz, NestedExp exp) {
		if (clazz.isPrimitive()) {
			return new StaticCallExp(ObjectConverter.toPrimitive, exp);
		}
		return new StaticCallExp(ObjectConverter.toObject, exp);
	}

	protected List<CtBehavior> getConsultedBehaviors(CtBehavior behavior) throws NotFoundException {
		List<CtBehavior> behaviors = new ArrayList<CtBehavior>(Collections.singleton(behavior));
		getLogger().debug(
				"behavior: " + behavior.getLongName() + " with declaring class "
						+ behavior.getDeclaringClass().getName());
		List<CtClass> consultedClasses = getConsultedTypes(behavior.getDeclaringClass());
		for (CtClass consultedClass : consultedClasses) {
			getLogger().debug("consulted class: " + consultedClass.getName());
			for (CtBehavior superBehavior : consultedClass.getDeclaredBehaviors()) {
				if (isOverridden(behavior, superBehavior)) {
					getLogger().info(
							"adding superBehavior: " + superBehavior.getLongName() + " with declaring class "
									+ superBehavior.getDeclaringClass().getName());
					behaviors.add(superBehavior);
				}
			}
		}
		return behaviors;
	}

	protected List<CtClass> getConsultedTypes(CtClass clazz) throws NotFoundException {
		List<CtClass> consultedClasses = new ArrayList<CtClass>();
		consultedClasses.addAll(getConsultedImmediateInterfaces(clazz));
		CtClass superClass = clazz.getSuperclass();
		while (superClass != null) {
			addConsultable(consultedClasses, superClass);
			consultedClasses.addAll(getConsultedImmediateInterfaces(superClass));
			superClass = superClass.getSuperclass();
		}
		return consultedClasses;
	}

	private List<CtClass> getConsultedImmediateInterfaces(CtClass clazz) throws NotFoundException {
		List<CtClass> consultedInterfaces = new ArrayList<CtClass>();
		for (CtClass interfaze : clazz.getInterfaces()) {
			addConsultable(consultedInterfaces, interfaze);
		}
		return consultedInterfaces;
	}

	private void addConsultable(List<CtClass> consultedClasses, CtClass clazz) {
		if (clazz.hasAnnotation(JOCEnabled.class)) {
			consultedClasses.add(clazz);
		}
	}

	public boolean isSynthetic(CtBehavior behavior) {
		return (AccessFlag.of(behavior.getModifiers()) & AccessFlag.SYNTHETIC) > 0;
	}

	private boolean isOverridden(CtBehavior behavior, CtBehavior superBehavior) throws NotFoundException {
		getLogger().debug(behavior.getLongName() + " --- " + superBehavior.getLongName());
		if (!behavior.getClass().equals(superBehavior.getClass())) {
			getLogger().debug("don't have the same type");
			return false;
		}
		if (behavior instanceof CtMethod) {
			boolean equals = behavior.equals(superBehavior);
			getLogger().debug("CtBehavior.equals says: " + equals);
			if (!equals && isOverriddenByGeneric(behavior, superBehavior)) {
				getLogger().debug("but isOverriddenByGeneric says true");
				return true;
			}
			return equals;
		}
		if (behavior.getParameterTypes().length != superBehavior.getParameterTypes().length) {
			getLogger().debug("not the same amount of parameters");
			return false;
		}
		for (int i = 0; i < behavior.getParameterTypes().length; i++) {
			if (!behavior.getParameterTypes()[i].equals(superBehavior.getParameterTypes()[i])) {
				getLogger().debug("parameter " + i + " has different type");
				return false;
			}
		}
		if (!isSubclass(behavior.getDeclaringClass(), superBehavior.getDeclaringClass())) {
			getLogger().debug("not in a superclass");
			return false;
		}
		if (!isSameOrLessRestrictive(behavior.getModifiers(), superBehavior.getModifiers())) {
			getLogger().debug("not less restrictive");
			return false;
		}
		if (Modifier.isPrivate(superBehavior.getModifiers()) || Modifier.isFinal(superBehavior.getModifiers())
				|| Modifier.isStatic(superBehavior.getModifiers())) {
			getLogger().debug("superBehavior has invalid modifiers");
			return false;
		}
		if (Modifier.isPrivate(behavior.getModifiers()) || Modifier.isStatic(behavior.getModifiers())) {
			getLogger().debug("behavior has invalid modifiers");
			return false;
		}
		if (Modifier.isPackage(superBehavior.getModifiers())
				&& !behavior.getDeclaringClass().getPackageName()
						.equals(superBehavior.getDeclaringClass().getPackageName())) {
			getLogger().debug("package visibility broken");
			return false;
		}
		return true;
	}

	private boolean isOverriddenByGeneric(CtBehavior behavior, CtBehavior superBehavior) throws NotFoundException {
		CtClass[] behaviorParams = behavior.getParameterTypes();
		CtClass[] superParams = superBehavior.getParameterTypes();
		if (behavior.getName().equals(superBehavior.getName()) && behaviorParams.length == superParams.length) {
			for (int i = 0; i < behaviorParams.length; i++) {
				if (!behaviorParams[i].equals(superParams[i])
						&& !(superParams[i].equals(transformer.getPool().get(Object.class.getName())))) {
					return false;
				}
			}
			for (CtBehavior possibleBehavior : behavior.getDeclaringClass().getDeclaredBehaviors()) {
				if (isSynthetic(possibleBehavior) && possibleBehavior.equals(superBehavior)) {
					return true;
				}
			}
		}
		return false;
	}

	private boolean isSameOrLessRestrictive(int modifiers, int superModifiers) {
		if (Modifier.isPublic(modifiers)) {
			return Modifier.isPublic(superModifiers) || Modifier.isProtected(superModifiers)
					|| Modifier.isPackage(superModifiers);
		}
		if (Modifier.isProtected(modifiers)) {
			return Modifier.isProtected(superModifiers) || Modifier.isPackage(superModifiers);
		}
		if (Modifier.isPackage(modifiers)) {
			return Modifier.isPackage(superModifiers);
		}
		return false;
	}

	private boolean isSubclass(CtClass clazz, CtClass superClass) throws NotFoundException {
		if (clazz.getSuperclass() == null) {
			return false;
		}
		if (clazz.getSuperclass().equals(superClass)) {
			return true;
		}
		return isSubclass(clazz.getSuperclass(), superClass);
	}

	protected List<NestedExp> getParamsForConditionMethod(CtBehavior behavior, CtMethod pcMethod, CtClass returnType)
			throws NotFoundException {
		if (pcMethod.getParameterTypes().length == 0) {
			return Collections.emptyList();
		}
		List<NestedExp> params = new ArrayList<NestedExp>();
		params.add(NestedExp.THIS);
		int allowedNumParams = 1;
		if (behavior != null) {
			allowedNumParams = behavior.getParameterTypes().length + 1;
		}
		int paramsOffset = 1;
		if (returnType != null && pcMethod.getParameterTypes().length > 1) {
			allowedNumParams++;
			paramsOffset++;
			params.add(handlePrimitiveExp(pcMethod.getParameterTypes()[1], NestedExp.RETURN_VALUE));
		}
		if (pcMethod.getParameterTypes().length > allowedNumParams) {
			throw new JOCTransformationException("method " + pcMethod.getLongName() + " can have no more than "
					+ allowedNumParams + " arguments");
		}
		for (int i = 1; i <= pcMethod.getParameterTypes().length - paramsOffset; i++) {
			params.add(handlePrimitiveExp(pcMethod.getParameterTypes()[i - 1 + paramsOffset], NestedExp.arg(i)));
		}
		return params;
	}

	public String getRecipientString(CtMember member) {
		if (member instanceof CtField) {
			return member.getDeclaringClass().getName() + "%" + member.getName();
		} else if (member instanceof CtConstructor) {
			return ((CtConstructor) member).getLongName();
		} else if (member instanceof CtMethod) {
			String longName = ((CtMethod) member).getLongName();
			String classAndMethodName = longName.substring(0, longName.indexOf("("));
			return longName.substring(0, classAndMethodName.lastIndexOf(".")) + "%"
					+ longName.substring(classAndMethodName.lastIndexOf(".") + 1);
		}
		return "Unknown";
	}

	private String getRecipientString(CtClass clazz) {
		return clazz.getName();
	}

	protected StandaloneExp callConditionString(CtClass contractClass, CtMethod pcMethod, boolean evaluate,
			CtBehavior behavior, CtClass referenceClass) throws NotFoundException {
		return callConditionString(contractClass, pcMethod, evaluate, behavior, false,
				getRecipientString(referenceClass));
	}

	protected StandaloneExp callConditionString(CtClass contractClass, CtMethod pcMethod, boolean evaluate,
			CtBehavior behavior, boolean useReturnType) throws NotFoundException {
		return callConditionString(contractClass, pcMethod, evaluate, behavior, useReturnType,
				getRecipientString(behavior));
	}

	private StandaloneExp callConditionString(CtClass contractClass, CtMethod pcMethod, boolean evaluate,
			CtBehavior behavior, boolean useReturnType, String referenceName) throws NotFoundException {
		CtClass returnType = null;
		if (useReturnType) {
			returnType = getReturnType(behavior);
		}
		List<NestedExp> params = getParamsForConditionMethod(behavior, pcMethod, returnType);
		NestedExp pcClassValue = new ValueExp(pcMethod.getDeclaringClass().getName());
		IfExp activeCondition = new IfExp(new StaticCallExp(ThreadAwareConditionCache.isConditionActive,
				NestedExp.THIS, pcClassValue));
		activeCondition.addIfBody(new StaticCallExp(ThreadAwareConditionCache.deactivateCondition, NestedExp.THIS,
				pcClassValue).toStandalone());
		NestedExp condition = new StaticCallExp(ThreadAwareConditionCache.getCondition, NestedExp.THIS, pcClassValue);
		NestedExp conditionCall = new CastExp(pcMethod.getDeclaringClass(), condition);
		conditionCall = conditionCall.appendCall(pcMethod.getName(), params);
		StandaloneExp activationCall = new StaticCallExp(ThreadAwareConditionCache.activateCondition, NestedExp.THIS,
				pcClassValue).toStandalone();
		if (evaluate) {
			NestedExp evaluationResult = new ConstructorExp(EvaluationResult.class, new ValueExp(referenceName),
					new ValueExp(contractClass), conditionCall, new ValueExp(pcMethod.getLongName()
							+ " returned false."));
			StandaloneExp evaluationCall = new StaticCallExp(Evaluator.evaluateContract, evaluationResult)
					.toStandalone();
			TryExp activationFallback = new TryExp(evaluationCall);
			activationFallback.addFinally(activationCall);
			activeCondition.addIfBody(activationFallback);
		} else {
			activeCondition.addIfBody(conditionCall.toStandalone());
			activeCondition.addIfBody(activationCall);
		}
		getLogger().info("callConditionString: " + activeCondition.getCode());
		return activeCondition;
	}

	protected CtClass getReturnType(CtBehavior behavior) throws NotFoundException {
		CtClass returnType = null;
		if (behavior != null && behavior instanceof CtMethod) {
			CtMethod method = (CtMethod) behavior;
			if (!method.getReturnType().equals(CtClass.voidType)) {
				returnType = ((CtMethod) behavior).getReturnType();
			}
		}
		return returnType;
	}

	protected StandaloneExp initializeConditionString(String pcClassName) {
		return initializeCache(NestedExp.THIS, new ValueExp(pcClassName), new ConstructorExp(pcClassName));
	}

	protected IfExp initializeCache(NestedExp objId, NestedExp stringId, NestedExp cached) {
		IfExp containsCondition = new IfExp(new BooleanExp(new StaticCallExp(
				ThreadAwareConditionCache.containsCondition, objId, stringId)).not());
		containsCondition.addIfBody(new StaticCallExp(ThreadAwareConditionCache.saveCondition, objId, stringId, cached)
				.toStandalone());
		return containsCondition;
	}

	public Logger getLogger() {
		return logger;
	}
}
